/*
 * Marmoset Add-In for Microsoft Visual Studio
 *
 * Copyright (C) 2007, David H. Hovemeyer
 * Copyright (C) 2007, Fidel K. Estrella
 *
 * This program is free software licensed under the
 * terms of the GNU General Public License, version 2.
 * See the file "COPYING.txt" for details.
 */

using System;
using System.IO;
using System.Collections.Generic;
using System.Text;
using ICSharpCode.SharpZipLib.Zip;

namespace Marmoset
{
    #region ZipCreatorException
    /// <summary>
    /// Exception to throw to indicate that we couldn't create
    /// the zip file.  Generally, this is because an input file
    /// not underneath the base directory has been added,
    /// and we don't know where to put that input file in
    /// the zip file.
    /// </summary>
    class ZipCreatorException : Exception
    {
        /// <summary>
        /// Constructor.
        /// </summary>
        /// <param name="msg">message explaining the reason for the exception</param>
        public ZipCreatorException(string msg) : base(msg)
        {
        }
    }
    #endregion

    /// <summary>
    /// Convenient interface for creating a zip file using
    /// SharpZipLib.
    /// </summary>
    class ZipCreator : IDisposable
    {
        #region Private Fields
        private string _outputFile;
        private string _baseDir;
        private LinkedList<string> _inputFileList;
        private DebugOutput _debugOut;
        private FileStream _fileStream;
        private bool _outputStreamClosed;
        #endregion

        #region Constructors
        /// <summary>
        /// Constructor.
        /// </summary>
        /// <param name="outputFile">name of zip file to create</param>
        /// <param name="baseDir">base directory - all input files are underneath</param>
        public ZipCreator(string outputFile, string baseDir)
        {
            this._outputFile = outputFile;
            this._baseDir = canonicalizeBaseDir(baseDir);
            this._inputFileList = new LinkedList<string>();
        }

        /// <summary>
        /// Constructor.
        /// </summary>
        /// <param name="outputFileStream">FileStream to which zip file should be written</param>
        /// <param name="baseDir">base directory - all input files are underneath</param>
        public ZipCreator(FileStream outputFileStream, string baseDir)
        {
            this._fileStream = outputFileStream;
            this._baseDir = canonicalizeBaseDir(baseDir);
            this._inputFileList = new LinkedList<string>();
        }
        #endregion

        /// <summary>
        /// This member needs a description. Also, place within a #region/#endregion block.
        /// </summary>
        /// <param name="baseDir"></param>
        /// <returns></returns>
        private static string canonicalizeBaseDir(string baseDir)
        {
            baseDir = Path.GetFullPath(baseDir);
            if (!baseDir.EndsWith(Path.DirectorySeparatorChar.ToString()))
            {
                baseDir += Path.DirectorySeparatorChar;
            }
            return baseDir;
        }

        #region Public Members
        /// <summary>
        /// Set DebugOutput.
        /// </summary>
        /// <param name="debugOut">the DebugOutput to use</param>
        public void SetDebugOutput(DebugOutput debugOut)
        {
            _debugOut = debugOut;
        }

        /// <summary>
        /// Add an input file to be included as part of the zip file.
        /// Must be underneath the base directory.
        /// </summary>
        /// <param name="inputFile">an input file to add to the zip file</param>
        public void AddInputFile(string inputFile)
        {
            WriteToOutput("Input file: " + inputFile);
            string inFileLower = inputFile.ToLower();
            string baseDirLower = _baseDir.ToLower();
            if (Path.IsPathRooted(inputFile))
            {
                // If this input file has an absolute path,
                // then it needs to be underneath the base directory.
                if (!inFileLower.StartsWith(baseDirLower))
                {
                    throw new ZipCreatorException("Input file " +
                        inputFile +
                        " is not underneath base directory " +
                        _baseDir);
                }

                // Make relative to base directory
                // TODO: Place these in a try/catch.
                inputFile = inputFile.Substring(_baseDir.Length);
                WriteToOutput("after making inputfile relative: " + inputFile);
            }
            _inputFileList.AddLast(inputFile);
        }

        /// <summary>
        /// Create the zip file.
        /// Throws an exception if unsuccessful.
        /// </summary>
        public void CreateZipFile()
        {
            // Create sorted array of filenames
            string[] fileList = new string[_inputFileList.Count];
            int count = 0;
            for (IEnumerator<string> e = _inputFileList.GetEnumerator(); e.MoveNext(); )
            {
                fileList[count++] = e.Current;
            }
            Array.Sort(fileList);

            // Open the output stream (if not already open)
            OpenFileStream();

            // Use a ZipOutputStream to write all of the input files to the zip file
            WriteToOutput("Creating zip file: " + _outputFile);
            using (ZipOutputStream zipOut = new ZipOutputStream(_fileStream))
            {
                zipOut.SetLevel(9);

                for (int i = 0; i < fileList.Length; i++)
                {
                    WriteZipEntry(zipOut, fileList[i]);
                }
            }
            _outputStreamClosed = true;
        }

        public void Dispose()
        {
            if (!_outputStreamClosed && _fileStream != null)
            {
                _fileStream.Close();
                _outputStreamClosed = true;
            }
        }
        #endregion

        #region Private Members
        private void OpenFileStream()
        {
            if (_fileStream == null)
            {
                _fileStream = File.OpenWrite(_outputFile);
            }
        }

        private void WriteZipEntry(ZipOutputStream zipOut, string inputFile)
        {
            WriteToOutput("Writing zip entry for " + inputFile);

            string separator = _baseDir.EndsWith(Path.DirectorySeparatorChar.ToString())
                    ? ""
                    : Path.DirectorySeparatorChar.ToString();
            string fullInputFilePath = _baseDir + separator + inputFile;

            byte[] buf = new byte[4096];
            int totalBytes = 0;
            using (FileStream fs = File.OpenRead(fullInputFilePath))
            {
                // Important: paths in a zip file should use forward
                // slashes as path separators, not backslashes
                string entryName = inputFile;
                entryName = entryName.Replace('\\', '/');

                ZipEntry entry = new ZipEntry(entryName);
                entry.Size = fs.Length; // Important!
                entry.DateTime = DateTime.Now;
                zipOut.PutNextEntry(entry);

                while (true)
                {
                    int numRead = fs.Read(buf, 0, buf.Length);
                    if (numRead <= 0)
                    {
                        break;
                    }
                    zipOut.Write(buf, 0, numRead);
                    totalBytes += numRead;
                }
            }

            WriteToOutput("Wrote " + totalBytes + " bytes to ZipOutputStream");
        }

        private void WriteToOutput(String msg)
        {
            if (_debugOut != null)
            {
                _debugOut.WriteToOutput(msg);
            }
        }
        #endregion
    }
}
